package com.xxl.job.executor.service.jobhandler;

import com.xxl.job.core.context.XxlJobHelper;
import com.xxl.job.core.handler.annotation.XxlJob;
import com.xxl.job.core.util.GsonTool;
import com.xxl.job.executor.model.Constants;
import com.xxl.job.executor.model.to.FtpParam;
import com.xxl.job.executor.utils.FileFilterUtil;
import io.micrometer.common.util.StringUtils;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPFile;
import org.apache.commons.net.ftp.FTPReply;
import org.springframework.stereotype.Component;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * XXL-Job FTP文件传输执行器
 * 功能说明：
 * 1. 支持FTP文件上传（UPLOAD）和下载（DOWNLOAD）操作
 * 2. 提供文件过滤机制（按文件名模式筛选）
 * 3. 支持重复文件跳过（基于大小+时间或MD5校验）
 * 4. 实现传输失败自动清理机制（本地/远程临时文件）
 * 5. 提供两种传输模式：断点续传（RESUME）和删除重建（默认）
 * 6. 支持本地与远程时区转换，确保文件时间校验准确性
 * 7. 支持时间通配符，使用{}包裹时间字段，如：{yyyyMMdd}
 */
@Component
public class FtpJob {

    /**
     * 增强版FTP文件传输任务入口
     * 注解说明：XXL-Job任务标识，对应任务配置中的JobHandler名称
     */
    @XxlJob("ftp")
    public void ftpJobHandler() {
        // 解析任务参数
        FtpParam param = paramParser(XxlJobHelper.getJobParam());
        if (null == param) {
            XxlJobHelper.log("参数解析失败，终止FTP操作");
            return;
        }

        try {
            // ===== 新增：解析路径中的日期时间占位符 =====
            String originalLocalPath = param.getLocalPath();
            String originalRemotePath = param.getRemotePath();

            // 解析本地路径
            String resolvedLocalPath = resolveDateTimePath(originalLocalPath,
                    ZoneId.of(param.getLocalTimezone()), param.getLocalTimeOffset());
            param.setLocalPath(resolvedLocalPath);

            // 解析远程路径
            String resolvedRemotePath = resolveDateTimePath(originalRemotePath,
                    ZoneId.of(param.getRemoteTimezone()), param.getRemoteTimeOffset());
            param.setRemotePath(resolvedRemotePath);

            // 打印任务基本信息
            XxlJobHelper.log("开始执行FTP操作: {}", param.getOperation());
            XxlJobHelper.log("服务器地址: {}:{}", param.getHost(), param.getPort());
            XxlJobHelper.log("传输配置 - 模式: {}, 比较方式: {}", param.getTransferMode(), param.getFileCompareMode());
            XxlJobHelper.log("时区配置 - 本地: {}, 远程: {}", param.getLocalTimezone(), param.getRemoteTimezone());
            // 打印解析结果日志
            XxlJobHelper.log("路径解析完成 - 本地路径: [{}] -> [{}]", originalLocalPath, resolvedLocalPath);
            XxlJobHelper.log("路径解析完成 - 远程路径: [{}] -> [{}]", originalRemotePath, resolvedRemotePath);
            XxlJobHelper.log("本地时间基准 - 时区: {}, 偏移: {}分钟, 实际时间: {}",
                    param.getLocalTimezone(), param.getLocalTimeOffset(),
                    ZonedDateTime.now(ZoneId.of(param.getLocalTimezone())).plusMinutes(param.getLocalTimeOffset()));
            XxlJobHelper.log("远程时间基准 - 时区: {}, 偏移: {}分钟, 实际时间: {}",
                    param.getRemoteTimezone(), param.getRemoteTimeOffset(),
                    ZonedDateTime.now(ZoneId.of(param.getRemoteTimezone())).plusMinutes(param.getRemoteTimeOffset()));
            // 创建并校验FTP连接
            FTPClient ftpClient = createFtpClient(param);
            if (ftpClient == null) {
                XxlJobHelper.log("FTP服务器连接失败，终止操作");
                return;
            }

            // 执行核心操作
            boolean success = executeFtpOperation(ftpClient, param);

            // 输出执行结果
            XxlJobHelper.log("FTP {}操作{}", param.getOperation(), success ? "成功" : "失败");

            // 关闭连接
            closeFtpClient(ftpClient);

        } catch (Exception e) {
            XxlJobHelper.log("FTP操作发生异常: {}", e.getMessage());
        }
    }

    // ------------------------------ 核心操作执行 ------------------------------

    /**
     * 根据操作类型执行对应的FTP操作（上传/下载）
     *
     * @param ftpClient FTP客户端实例
     * @param param     传输参数配置
     * @return 操作是否成功
     */
    private boolean executeFtpOperation(FTPClient ftpClient, FtpParam param) {
        if (Constants.FtpOperation.UPLOAD.equalsIgnoreCase(param.getOperation())) {
            return putFilesByRule(ftpClient, param);
        } else if (Constants.FtpOperation.DOWNLOAD.equalsIgnoreCase(param.getOperation())) {
            return getFilesByRule(ftpClient, param);
        } else {
            XxlJobHelper.log("不支持的操作类型: {}", param.getOperation());
            return false;
        }
    }

    /**
     * 按规则批量下载文件（支持重复校验和传输模式）
     *
     * @param ftpClient FTP客户端实例
     * @param param     传输参数配置
     * @return 下载操作是否成功
     */
    private boolean getFilesByRule(FTPClient ftpClient, FtpParam param) {
        try {
            String remotePath = param.getRemotePath();
            String localPath = param.getLocalPath();

            // 获取远程目录文件并筛选
            FTPFile[] allRemoteFiles = ftpClient.listFiles(remotePath);
            if (allRemoteFiles == null || allRemoteFiles.length == 0) {
                XxlJobHelper.log("远程目录不存在或为空: {}", remotePath);
                return false;
            }

            List<FTPFile> matchedFiles = FileFilterUtil.ftpFileFilter(allRemoteFiles, param);
            if (matchedFiles.isEmpty()) {
                XxlJobHelper.log("未找到符合条件的远程文件");
                return true; // 无符合文件视为成功（非失败）
            }

            XxlJobHelper.log("找到{}个符合条件的文件，开始下载...", matchedFiles.size());

            // 批量处理下载
            int successCount = 0;
            for (FTPFile remoteFile : matchedFiles) {
                String fileName = remoteFile.getName();
                String remoteFilePath = buildRemoteFilePath(remotePath, fileName);
                String localFilePath = buildLocalFilePath(localPath, fileName);
                File localFile = new File(localFilePath);

                // 重复文件校验：已存在且一致则跳过
                String remoteTimeStr = ftpClient.getModificationTime(remoteFilePath);
                if (localFile.exists() && isFileConsistent(localFile, remoteFile, param, remoteTimeStr)) {
                    XxlJobHelper.log("文件已存在且一致，跳过下载: {}", fileName);
                    continue;
                }

                // 执行单文件下载
                if (getFile(ftpClient, param, localFilePath, remoteFilePath)) {
                    successCount++;
                } else {
                    cleanupFailedFile(localFile, "下载"); // 下载失败清理本地文件
                }
            }

            XxlJobHelper.log("下载完成 - 成功: {}个, 失败: {}个",
                    successCount, matchedFiles.size() - successCount);
            return successCount > 0;

        } catch (Exception e) {
            XxlJobHelper.log("批量下载发生异常: {}", e.getMessage());
            return false;
        }
    }

    /**
     * 按规则批量上传文件（支持重复校验和传输模式）
     *
     * @param ftpClient FTP客户端实例
     * @param param     传输参数配置
     * @return 上传操作是否成功
     */
    private boolean putFilesByRule(FTPClient ftpClient, FtpParam param) {
        try {
            String remotePath = param.getRemotePath();
            String localPath = param.getLocalPath();

            // 获取本地目录文件并筛选
            File localDir = new File(localPath);
            File[] allLocalFiles = localDir.listFiles();
            if (allLocalFiles == null || allLocalFiles.length == 0) {
                XxlJobHelper.log("本地目录不存在或为空: {}", localPath);
                return false;
            }

            List<File> matchedFiles = FileFilterUtil.fileFilter(allLocalFiles, param);
            if (matchedFiles.isEmpty()) {
                XxlJobHelper.log("未找到符合条件的本地文件");
                return true; // 无符合文件视为成功（非失败）
            }

            XxlJobHelper.log("找到{}个符合条件的文件，开始上传...", matchedFiles.size());

            // 批量处理上传
            int successCount = 0;
            for (File localFile : matchedFiles) {
                String fileName = localFile.getName();
                String remoteFilePath = buildRemoteFilePath(remotePath, fileName);

                // 重复文件校验：远程已存在且一致则跳过
                FTPFile remoteFile = getRemoteFileByName(ftpClient, remotePath, fileName);
                String remoteTimeStr = ftpClient.getModificationTime(remoteFilePath);
                if (remoteFile != null && isFileConsistent(localFile, remoteFile, param, remoteTimeStr)) {
                    XxlJobHelper.log("文件已存在且一致，跳过上传: {}", fileName);
                    continue;
                }

                // 执行单文件上传
                if (putFile(ftpClient, param, localFile.getAbsolutePath(), remoteFilePath)) {
                    successCount++;
                } else {
                    cleanupFailedRemoteFile(ftpClient, remoteFilePath, "上传"); // 上传失败清理远程文件
                }
            }

            XxlJobHelper.log("上传完成 - 成功: {}个, 失败: {}个",
                    successCount, matchedFiles.size() - successCount);
            return successCount > 0;

        } catch (Exception e) {
            XxlJobHelper.log("批量上传发生异常: {}", e.getMessage());
            return false;
        }
    }

    // ------------------------------ 单文件传输实现 ------------------------------

    /**
     * 下载单个文件（支持断点续传/删除重建模式）
     *
     * @param ftpClient      FTP客户端实例
     * @param param          传输参数配置
     * @param localFilePath  本地文件路径
     * @param remoteFilePath 远程文件路径
     * @return 下载是否成功
     */
    private boolean getFile(FTPClient ftpClient, FtpParam param, String localFilePath, String remoteFilePath) {
        try {
            File localFile = new File(localFilePath);
            createLocalDir(localFile); // 确保本地目录存在

            // 获取远程文件大小
            long remoteFileSize = getRemoteFileSize(ftpClient, remoteFilePath);
            if (remoteFileSize < 0) {
                XxlJobHelper.log("无法获取远程文件大小: {}", remoteFilePath);
                return false;
            }

            // 断点续传模式：文件存在且不完整，且服务器支持断点续传
            if (Constants.TransferMode.RESUME.equals(param.getTransferMode())
                    && localFile.exists() && localFile.length() > 0
                    && localFile.length() < remoteFileSize
                    && ftpClient.hasFeature("REST")) {
                return resumeDownload(ftpClient, param, remoteFilePath, localFile, remoteFileSize);
            }

            // 默认：删除重建模式
            return deleteAndRecreateDownload(ftpClient, param, remoteFilePath, localFile, remoteFileSize);

        } catch (Exception e) {
            XxlJobHelper.log("文件下载异常 [{}]: {}", remoteFilePath, e.getMessage());
            return false;
        }
    }

    /**
     * 上传单个文件（支持断点续传/删除重建模式）
     *
     * @param ftpClient      FTP客户端实例
     * @param param          传输参数配置
     * @param localFilePath  本地文件路径
     * @param remoteFilePath 远程文件路径
     * @return 上传是否成功
     */
    private boolean putFile(FTPClient ftpClient, FtpParam param, String localFilePath, String remoteFilePath) {
        String fileName = new File(localFilePath).getName();
        try {
            File localFile = new File(localFilePath);
            if (!localFile.exists() || !localFile.isFile()) {
                XxlJobHelper.log("本地文件不存在或不是文件: {}", localFilePath);
                return false;
            }

            // 确保远程目录存在
            String remotePath = param.getRemotePath();
            if (!ftpClient.changeWorkingDirectory(remotePath) && !createRemoteDirectory(ftpClient, remotePath)) {
                XxlJobHelper.log("无法访问或创建远程目录: {}", remotePath);
                return false;
            }

            // 获取文件大小信息
            long localFileSize = localFile.length();
            FTPFile remoteFile = getRemoteFileByName(ftpClient, remotePath, fileName);
            long remoteFileSize = (remoteFile != null) ? remoteFile.getSize() : 0;

            // 断点续传模式：远程文件存在且不完整，且服务器支持断点续传
            if (Constants.TransferMode.RESUME.equals(param.getTransferMode())
                    && remoteFile != null && remoteFileSize > 0
                    && remoteFileSize < localFileSize
                    && ftpClient.hasFeature("REST")) {
                return resumeUpload(ftpClient, param, localFile, remoteFilePath, remoteFileSize);
            }

            // 默认：删除重建模式
            return deleteAndRecreateUpload(ftpClient, param, localFile, remoteFilePath, localFileSize);

        } catch (Exception e) {
            XxlJobHelper.log("文件上传异常 [{}]: {}", fileName, e.getMessage());
            return false;
        }
    }

    // ------------------------------ 传输模式实现 ------------------------------

    /**
     * 断点续传下载实现
     *
     * @param ftpClient      FTP客户端实例
     * @param remoteFilePath 远程文件路径
     * @param localFile      本地文件对象
     * @param remoteFileSize 远程文件总大小
     * @return 续传是否成功
     */
    private boolean resumeDownload(FTPClient ftpClient, FtpParam param, String remoteFilePath,
                                   File localFile, long remoteFileSize) throws IOException {
        long localSize = localFile.length();
        XxlJobHelper.log("启动断点续传下载 [{}] (从{}字节开始)", remoteFilePath, localSize);

        try {
            // 设置断点续传偏移量
            ftpClient.setRestartOffset(localSize);
            int reply = ftpClient.getReplyCode();
            if (!(reply >= 200 && reply < 300)) {
                XxlJobHelper.log("设置偏移量失败（响应码: {}），降级为删除重建模式", reply);
                return deleteAndRecreateDownload(ftpClient, param, remoteFilePath, localFile, remoteFileSize);
            }
        } catch (IOException e) {
            XxlJobHelper.log("设置偏移量异常: {}，降级为删除重建模式", e.getMessage());
            return deleteAndRecreateDownload(ftpClient, param, remoteFilePath, localFile, remoteFileSize);
        }

        // 执行续传
        try (RandomAccessFile raf = new RandomAccessFile(localFile, "rw")) {
            raf.seek(localSize); // 定位到断点位置
            boolean success = ftpClient.retrieveFile(remoteFilePath, new FileOutputStream(raf.getFD()));

            // 校验文件完整性
            if (success && localFile.length() == remoteFileSize) {
                XxlJobHelper.log("断点续传下载成功 [{}] -> [{}]", remoteFilePath, localFile.getAbsolutePath());
                changeLocalFileTime(ftpClient, param, localFile, remoteFilePath); // 同步文件时间
                return true;
            } else {
                XxlJobHelper.log("续传不完整 [{}]（实际: {}字节, 预期: {}字节）",
                        remoteFilePath, localFile.length(), remoteFileSize);
                cleanupFailedFile(localFile, "断点续传下载");
                return false;
            }
        }
    }

    /**
     * 删除重建下载实现（完整下载）
     *
     * @param ftpClient      FTP客户端实例
     * @param remoteFilePath 远程文件路径
     * @param localFile      本地文件对象
     * @param remoteFileSize 远程文件总大小
     * @return 下载是否成功
     */
    private boolean deleteAndRecreateDownload(FTPClient ftpClient, FtpParam param, String remoteFilePath,
                                              File localFile, long remoteFileSize) throws IOException {
        // 删除已存在的不完整文件
        if (localFile.exists() && !localFile.delete()) {
            XxlJobHelper.log("无法删除已存在文件: {}", localFile.getAbsolutePath());
            return false;
        }

        // 完整下载文件
        try (OutputStream outputStream = new FileOutputStream(localFile)) {
            boolean success = ftpClient.retrieveFile(remoteFilePath, outputStream);
            if (success && localFile.length() == remoteFileSize) {
                XxlJobHelper.log("完整下载成功 [{}] -> [{}]", remoteFilePath, localFile.getAbsolutePath());
                changeLocalFileTime(ftpClient, param, localFile, remoteFilePath); // 同步文件时间
                return true;
            } else {
                XxlJobHelper.log("完整下载失败 [{}]（服务器响应: {}）",
                        remoteFilePath, ftpClient.getReplyString());
                cleanupFailedFile(localFile, "完整下载");
                return false;
            }
        }
    }

    /**
     * 断点续传上传实现
     *
     * @param ftpClient      FTP客户端实例
     * @param localFile      本地文件对象
     * @param remoteFilePath 远程文件路径
     * @param remoteFileSize 远程文件当前大小（断点位置）
     * @return 续传是否成功
     */
    private boolean resumeUpload(FTPClient ftpClient, FtpParam param, File localFile,
                                 String remoteFilePath, long remoteFileSize) throws IOException {
        XxlJobHelper.log("启动断点续传上传 [{}] (从{}字节开始)", localFile.getName(), remoteFileSize);

        try {
            // 设置断点续传偏移量
            ftpClient.setRestartOffset(remoteFileSize);
            int reply = ftpClient.getReplyCode();
            if (!(reply >= 200 && reply < 300)) {
                XxlJobHelper.log("设置偏移量失败（响应码: {}），降级为删除重建模式", reply);
                return deleteAndRecreateUpload(ftpClient, param, localFile, remoteFilePath, localFile.length());
            }
        } catch (IOException e) {
            XxlJobHelper.log("设置偏移量异常: {}，降级为删除重建模式", e.getMessage());
            return deleteAndRecreateUpload(ftpClient, param, localFile, remoteFilePath, localFile.length());
        }

        // 执行续传
        try (RandomAccessFile raf = new RandomAccessFile(localFile, "r")) {
            raf.seek(remoteFileSize); // 定位到断点位置
            boolean success = ftpClient.storeFile(remoteFilePath, new FileInputStream(raf.getFD()));

            // 校验文件完整性
            FTPFile newRemoteFile = getRemoteFileByName(ftpClient, param.getRemotePath(), localFile.getName());
            if (success && newRemoteFile != null && newRemoteFile.getSize() == localFile.length()) {
                XxlJobHelper.log("断点续传上传成功 [{}] -> [{}]",
                        localFile.getName(), param.getRemotePath());
                changeRemoteFileTime(ftpClient, param, localFile); // 同步文件时间
                return true;
            } else {
                XxlJobHelper.log("续传不完整 [{}]（实际: {}字节）",
                        localFile.getName(), (newRemoteFile != null ? newRemoteFile.getSize() : 0));
                cleanupFailedRemoteFile(ftpClient, remoteFilePath, "断点续传上传");
                return false;
            }
        }
    }

    /**
     * 删除重建上传实现（完整上传）
     *
     * @param ftpClient      FTP客户端实例
     * @param localFile      本地文件对象
     * @param remoteFilePath 远程文件路径
     * @param localFileSize  本地文件总大小
     * @return 上传是否成功
     */
    private boolean deleteAndRecreateUpload(FTPClient ftpClient, FtpParam param, File localFile,
                                            String remoteFilePath, long localFileSize) throws IOException {
        String fileName = localFile.getName();
        // 删除已存在的远程文件
        FTPFile existingRemoteFile = getRemoteFileByName(ftpClient, param.getRemotePath(), fileName);
        if (existingRemoteFile != null && !ftpClient.deleteFile(remoteFilePath)) {
            XxlJobHelper.log("无法删除远程文件: {}", remoteFilePath);
            return false;
        }

        // 完整上传文件
        try (InputStream inputStream = new FileInputStream(localFile)) {
            boolean success = ftpClient.storeFile(remoteFilePath, inputStream);
            FTPFile newRemoteFile = getRemoteFileByName(ftpClient, param.getRemotePath(), fileName);

            if (success && newRemoteFile != null && newRemoteFile.getSize() == localFileSize) {
                XxlJobHelper.log("完整上传成功 [{}] -> [{}]", fileName, param.getRemotePath());
                changeRemoteFileTime(ftpClient, param, localFile); // 同步文件时间
                return true;
            } else {
                XxlJobHelper.log("完整上传失败 [{}]（服务器响应: {}）",
                        fileName, ftpClient.getReplyString());
                cleanupFailedRemoteFile(ftpClient, remoteFilePath, "完整上传");
                return false;
            }
        }
    }

    // ------------------------------ 工具方法 ------------------------------

    /**
     * 检查本地与远程文件是否一致
     * 支持两种模式：
     * 1. SIZE_TIME：文件大小一致 + 时间差在1秒内（时区转换后）
     * 2. MD5：需额外实现MD5校验逻辑（当前默认仅校验大小）
     *
     * @param localFile  本地文件
     * @param remoteFile 远程文件
     * @param param      传输参数配置
     * @return 是否一致
     */
    private boolean isFileConsistent(File localFile, FTPFile remoteFile, FtpParam param, String remoteTimeStr) {
        // 基础校验：文件存在性
        if (localFile == null || !localFile.exists() || remoteFile == null) {
            XxlJobHelper.log("文件不存在，无法进行一致性校验");
            return false;
        }

        // 第一步：校验文件大小
        if (localFile.length() != remoteFile.getSize()) {
            XxlJobHelper.log("文件大小不一致 [{}] - 本地: {}字节, 远程: {}字节",
                    localFile.getName(), localFile.length(), remoteFile.getSize());
            return false;
        }

        // 第二步：根据比较模式校验时间或其他属性
        if (Constants.CompareMode.SIZE_TIME.equals(param.getFileCompareMode())) {
            return checkTimeConsistency(localFile, param, remoteTimeStr);
        } else if (Constants.CompareMode.MD5.equals(param.getFileCompareMode())) {
            XxlJobHelper.log("MD5比较模式暂未实现，默认仅校验大小一致");
            return true; // 实际项目需实现MD5校验
        }

        return false;
    }

    /**
     * 校验本地与远程文件时间一致性（时区转换后）
     *
     * @param localFile 本地文件
     * @param param     传输参数配置
     * @return 时间是否一致（允许1秒内误差）
     */
    private boolean checkTimeConsistency(File localFile, FtpParam param, String remoteTimeStr) {
        // 解析本地文件时间（转换为本地时区）
        ZonedDateTime localZoneTime = ZonedDateTime.ofInstant(
                Instant.ofEpochMilli(localFile.lastModified()),
                ZoneId.of(param.getLocalTimezone())
        );


        // 解析远程文件时间（转换为远程时区）
        ZonedDateTime remoteZoneTime;
        try {
            SimpleDateFormat remoteTimeFormat = new SimpleDateFormat("yyyyMMddHHmmss");
            remoteTimeFormat.setTimeZone(TimeZone.getTimeZone(param.getRemoteTimezone()));
            Date remoteDate = remoteTimeFormat.parse(remoteTimeStr);

            remoteZoneTime = ZonedDateTime.ofInstant(
                    Instant.ofEpochMilli(remoteDate.getTime()),
                    ZoneId.of(param.getRemoteTimezone())
            );
        } catch (ParseException e) {
            throw new RuntimeException("远程时间解析失败: " + remoteTimeStr, e);
        }

        // 计算时间差并判断一致性（允许3秒误差）
        long timeDiff = Math.abs(ChronoUnit.MILLIS.between(localZoneTime, remoteZoneTime));
        boolean timeConsistent = timeDiff < 3000;

        if (!timeConsistent) {
            XxlJobHelper.log("文件时间不一致 [{}] - 本地: {}({}), 远程: {}({}), 差值: {}ms",
                    localFile.getName(),
                    localZoneTime, param.getLocalTimezone(),
                    remoteZoneTime, param.getRemoteTimezone(),
                    timeDiff);
        }

        return timeConsistent;
    }


    /**
     * 清理下载失败的本地文件
     *
     * @param file      本地文件对象
     * @param operation 操作描述（如下载/续传）
     */
    private void cleanupFailedFile(File file, String operation) {
        if (file.exists() && file.delete()) {
            XxlJobHelper.log("{}失败，已清理本地文件: {}", operation, file.getAbsolutePath());
        } else if (file.exists()) {
            XxlJobHelper.log("{}失败，清理本地文件失败: {}", operation, file.getAbsolutePath());
        }
    }

    /**
     * 清理上传失败的远程文件
     *
     * @param ftpClient      FTP客户端实例
     * @param remoteFilePath 远程文件路径
     * @param operation      操作描述（如上传/续传）
     */
    private void cleanupFailedRemoteFile(FTPClient ftpClient, String remoteFilePath, String operation) {
        try {
            if (ftpClient.deleteFile(remoteFilePath)) {
                XxlJobHelper.log("{}失败，已清理远程文件: {}", operation, remoteFilePath);
            } else {
                XxlJobHelper.log("{}失败，清理远程文件失败: {}", operation, remoteFilePath);
            }
        } catch (IOException e) {
            XxlJobHelper.log("{}失败，清理远程文件异常: {}", operation, e.getMessage());
        }
    }

    /**
     * 获取远程文件大小
     *
     * @param ftpClient      FTP客户端实例
     * @param remoteFilePath 远程文件路径
     * @return 文件大小（字节），-1表示获取失败
     */
    private long getRemoteFileSize(FTPClient ftpClient, String remoteFilePath) throws IOException {
        FTPFile[] files = ftpClient.listFiles(remoteFilePath);
        return (files != null && files.length > 0 && files[0].isFile()) ? files[0].getSize() : -1;
    }

    /**
     * 根据文件名查询远程文件
     *
     * @param ftpClient FTP客户端实例
     * @param remoteDir 远程目录
     * @param fileName  文件名
     * @return FTPFile对象，null表示未找到
     */
    private FTPFile getRemoteFileByName(FTPClient ftpClient, String remoteDir, String fileName) throws IOException {
        FTPFile[] files = ftpClient.listFiles(remoteDir);
        if (files != null) {
            for (FTPFile file : files) {
                if (file.getName().equals(fileName) && file.isFile()) {
                    return file;
                }
            }
        }
        return null;
    }

    /**
     * 构建本地文件完整路径
     *
     * @param localDir 本地目录
     * @param fileName 文件名
     * @return 本地文件路径
     */
    private String buildLocalFilePath(String localDir, String fileName) {
        return localDir + File.separator + fileName;
    }

    /**
     * 构建远程文件完整路径
     *
     * @param remoteDir 远程目录
     * @param fileName  文件名
     * @return 远程文件路径
     */
    private String buildRemoteFilePath(String remoteDir, String fileName) {
        return remoteDir + ("/".equals(remoteDir) ? "" : "/") + fileName;
    }

    /**
     * 同步远程文件时间为本地文件时间（上传场景）
     *
     * @param ftpClient FTP客户端实例
     * @param param     传输参数配置
     * @param localFile 本地文件（时间源）
     */
    private void changeRemoteFileTime(FTPClient ftpClient, FtpParam param, File localFile) {
        String remoteFilePath = buildRemoteFilePath(param.getRemotePath(), localFile.getName());
        Date localFileDate = new Date(localFile.lastModified());

        // 准备时间格式（远程时区）
        SimpleDateFormat remoteTimeFormat = new SimpleDateFormat("yyyyMMddHHmmss");
        remoteTimeFormat.setTimeZone(TimeZone.getTimeZone(param.getRemoteTimezone()));
        String timeString = remoteTimeFormat.format(localFileDate);
        long unixTimestamp = localFile.lastModified() / 1000; // 转换为秒级时间戳

        // 尝试多种MFMT命令格式（不同服务器支持差异）
        String[] commands = {
                String.format("MFMT %s %s", timeString, remoteFilePath),
                String.format("MFMT %d %s", unixTimestamp, remoteFilePath)
        };

        for (String command : commands) {
            try {
                int replyCode = ftpClient.sendCommand(command);
                String replyString = ftpClient.getReplyString().trim();

                if (replyCode >= 200 && replyCode < 300) {
                    XxlJobHelper.log("成功同步远程文件时间 [{}]（命令: {}, 响应: {}）",
                            remoteFilePath, command, replyString);
                    return;
                } else {
                    XxlJobHelper.log("同步远程时间失败 [{}]（命令: {}, 响应码: {}, 信息: {}）",
                            remoteFilePath, command, replyCode, replyString);
                }
            } catch (IOException e) {
                XxlJobHelper.log("同步远程时间异常 [{}]（命令: {}, 异常: {}）",
                        remoteFilePath, command, e.getMessage());
                return; // IO异常时终止尝试
            }
        }

        XxlJobHelper.log("所有时间同步命令均不支持，无法同步远程文件时间 [{}]", remoteFilePath);
    }

    /**
     * 同步本地文件时间为远程文件时间（下载场景）
     *
     * @param ftpClient      FTP客户端实例
     * @param param          传输参数配置
     * @param localFile      本地文件（目标）
     * @param remoteFilePath 远程文件（时间源）
     */
    private void changeLocalFileTime(FTPClient ftpClient, FtpParam param, File localFile, String remoteFilePath) {
        try {
            // 获取远程文件时间字符串
            String remoteTimeStr = ftpClient.getModificationTime(remoteFilePath);
            if (StringUtils.isBlank(remoteTimeStr)) {
                XxlJobHelper.log("获取远程文件时间失败 [{}]", remoteFilePath);
                return;
            }

            // 处理时间字符串，只保留前14位（去除可能的毫秒部分）
            String timeStr = remoteTimeStr.length() >= 14 ? remoteTimeStr.substring(0, 14) : remoteTimeStr;

            // 验证处理后的时间格式是否有效
            if (timeStr.length() != 14) {
                XxlJobHelper.log("远程时间格式无效 [{}]（原始值: {}）", remoteFilePath, remoteTimeStr);
                return;
            }

            // 使用固定格式解析（仅yyyymmddhhmmss）
            SimpleDateFormat remoteTimeFormat = new SimpleDateFormat("yyyyMMddHHmmss");
            remoteTimeFormat.setTimeZone(TimeZone.getTimeZone(param.getRemoteTimezone()));

            // 解析时间并同步到本地文件
            Date remoteDate = remoteTimeFormat.parse(timeStr);
            boolean syncSuccess = localFile.setLastModified(remoteDate.getTime());

            if (syncSuccess) {
                XxlJobHelper.log("成功同步本地文件时间 [{}] -> [{}]（时间: {}）",
                        remoteFilePath, localFile.getAbsolutePath(), remoteTimeFormat.format(remoteDate));
            } else {
                XxlJobHelper.log("同步本地文件时间失败 [{}]", localFile.getAbsolutePath());
            }

        } catch (ParseException e) {
            XxlJobHelper.log("时间解析失败 [{}]（异常: {}）", remoteFilePath, e.getMessage());
        } catch (IOException e) {
            XxlJobHelper.log("获取远程时间异常 [{}]（异常: {}）", remoteFilePath, e.getMessage());
        } catch (Exception e) {
            XxlJobHelper.log("同步本地时间未知异常 [{}]（异常: {}）", remoteFilePath, e.getMessage());
        }
    }

    /**
     * 创建本地目录（若不存在）
     *
     * @param file 目标文件（用于获取父目录）
     */
    private void createLocalDir(File file) {
        if (file == null) {
            XxlJobHelper.log("创建本地目录失败：文件对象为null");
            return;
        }

        File parentDir = file.getParentFile();
        if (parentDir == null) {
            XxlJobHelper.log("无需创建目录：文件无父目录");
            return;
        }

        if (parentDir.exists()) {
            XxlJobHelper.log("本地目录已存在：{}", parentDir.getAbsolutePath());
            return;
        }

        // 递归创建目录
        boolean isCreated = parentDir.mkdirs();
        if (isCreated) {
            XxlJobHelper.log("本地目录创建成功：{}", parentDir.getAbsolutePath());
        } else {
            XxlJobHelper.log("警告：本地目录创建失败，可能导致后续操作异常：{}", parentDir.getAbsolutePath());
        }
    }

    /**
     * 创建远程目录（支持多级目录，若不存在）
     *
     * @param ftpClient FTP客户端实例
     * @param remoteDir 远程目录路径
     * @return 是否创建成功
     */
    private static boolean createRemoteDirectory(FTPClient ftpClient, String remoteDir) throws IOException {
        String[] dirs = remoteDir.split("/");
        String currentDir = "";

        for (String dir : dirs) {
            if (dir.isEmpty()) continue; // 跳过空字符串（处理根目录场景）
            currentDir += "/" + dir;

            // 目录不存在则创建
            if (!ftpClient.changeWorkingDirectory(currentDir) && !ftpClient.makeDirectory(currentDir)) {
                return false;
            }
            ftpClient.changeWorkingDirectory(currentDir); // 进入创建的目录
        }
        return true;
    }

    /**
     * 解析路径中的日期时间占位符（{yyyy}、{MM}等）
     *
     * @param originalPath 原始路径（含占位符）
     * @return 替换后的实际路径
     */
    private String resolveDateTimePath(String originalPath, ZoneId zoneId, int offset) {
        if (StringUtils.isBlank(originalPath)) {
            return originalPath;
        }

        // 1. 获取基准时间（当前时间 + 偏移量，基于指定时区）
        ZonedDateTime baseTime = ZonedDateTime.now(ZoneId.of(zoneId.getId()))
                .plusMinutes(offset);

        // 2. 定义占位符映射关系（key: 占位符文本，value: 对应的值）
        Map<String, String> placeholderMap = new HashMap<>();
        placeholderMap.put("yyyy", baseTime.format(DateTimeFormatter.ofPattern("yyyy"))); // 年（4位）
        placeholderMap.put("MM", baseTime.format(DateTimeFormatter.ofPattern("MM")));     // 月（2位）
        placeholderMap.put("dd", baseTime.format(DateTimeFormatter.ofPattern("dd")));     // 日（2位）
        placeholderMap.put("HH", baseTime.format(DateTimeFormatter.ofPattern("HH")));     // 时（24小时制，2位）
        placeholderMap.put("hh", baseTime.format(DateTimeFormatter.ofPattern("hh")));     // 时（12小时制，2位）
        placeholderMap.put("mm", baseTime.format(DateTimeFormatter.ofPattern("mm")));     // 分（2位）
        placeholderMap.put("yyyyMM", baseTime.format(DateTimeFormatter.ofPattern("yyyyMM")));      // 年月
        placeholderMap.put("yyyyMMdd", baseTime.format(DateTimeFormatter.ofPattern("yyyyMMdd")));    // 年月日
        placeholderMap.put("yyyymmddHH", baseTime.format(DateTimeFormatter.ofPattern("yyyyMMddHH"))); // 年月日时

        // 3. 正则匹配所有{xxx}占位符并替换
        String resolvedPath = originalPath;
        Pattern pattern = Pattern.compile("\\{([a-zA-Z]+)}"); // 匹配{xxx}格式的占位符
        Matcher matcher = pattern.matcher(resolvedPath);

        while (matcher.find()) {
            String fullPlaceholder = matcher.group(0); // 完整占位符（如{yyyy}）
            String placeholderKey = matcher.group(1);  // 占位符内容（如yyyy）

            // 替换已定义的占位符，未定义的保留原样
            if (placeholderMap.containsKey(placeholderKey)) {
                resolvedPath = resolvedPath.replace(fullPlaceholder, placeholderMap.get(placeholderKey));
                XxlJobHelper.log("替换占位符: {} -> {}", fullPlaceholder, placeholderMap.get(placeholderKey));
            } else {
                XxlJobHelper.log("忽略未识别的占位符: {}", fullPlaceholder);
            }
        }

        return resolvedPath;
    }

    // ------------------------------ FTP连接管理 ------------------------------

    /**
     * 创建并初始化FTP客户端连接
     *
     * @param param 连接参数配置
     * @return FTPClient实例，null表示连接失败
     */
    private static FTPClient createFtpClient(FtpParam param) {
        FTPClient ftpClient = new FTPClient();
        try {
            // 连接服务器
            ftpClient.connect(param.getHost(), param.getPort());
            int replyCode = ftpClient.getReplyCode();
            if (!FTPReply.isPositiveCompletion(replyCode)) {
                XxlJobHelper.log("FTP服务器拒绝连接（响应码: {}）", replyCode);
                ftpClient.disconnect();
                return null;
            }

            // 登录验证
            if (!ftpClient.login(param.getUsername(), param.getPassword())) {
                XxlJobHelper.log("FTP登录失败（用户名或密码错误）");
                ftpClient.disconnect();
                return null;
            }

            // 初始化客户端配置
            ftpClient.setFileType(FTPClient.BINARY_FILE_TYPE); // 二进制传输模式
            ftpClient.setControlEncoding(StandardCharsets.UTF_8.name()); // 编码设置
            ftpClient.enterLocalPassiveMode(); // 启用被动模式（防火墙兼容）
            return ftpClient;

        } catch (Exception e) {
            XxlJobHelper.log("FTP连接创建失败（异常: {}）", e.getMessage());
            closeFtpClient(ftpClient); // 确保连接关闭
            return null;
        }
    }

    /**
     * 关闭FTP客户端连接
     *
     * @param ftpClient FTPClient实例
     */
    private static void closeFtpClient(FTPClient ftpClient) {
        try {
            if (ftpClient != null && ftpClient.isConnected()) {
                ftpClient.logout(); // 登出
                ftpClient.disconnect(); // 断开连接
            }
        } catch (IOException e) {
            XxlJobHelper.log("关闭FTP连接异常: {}", e.getMessage());
        }
    }

    // ------------------------------ 参数解析与校验 ------------------------------

    /**
     * 解析并校验FTP任务参数
     *
     * @param paramJson JSON格式的任务参数
     * @return FtpParam实例，null表示解析或校验失败
     */
    private static FtpParam paramParser(String paramJson) {
        // 基础非空校验
        if (paramJson == null || paramJson.trim().isEmpty()) {
            XxlJobHelper.log("任务参数不能为空（需提供JSON格式）");
            return null;
        }

        // JSON解析
        paramJson = paramJson.replace("\\", "\\\\"); // 处理转义字符
        FtpParam param = GsonTool.fromJson(paramJson, FtpParam.class);
        if (param == null) {
            XxlJobHelper.log("参数解析失败（JSON格式错误）");
            return null;
        }

        // 核心参数校验
        if (StringUtils.isBlank(param.getHost()) || param.getPort() <= 0) {
            XxlJobHelper.log("参数校验失败：FTP服务器地址和端口不能为空");
            return null;
        }
        if (StringUtils.isBlank(param.getUsername()) || StringUtils.isBlank(param.getPassword())) {
            XxlJobHelper.log("参数校验失败：用户名和密码不能为空");
            return null;
        }
        if (StringUtils.isBlank(param.getOperation())) {
            XxlJobHelper.log("参数校验失败：操作类型(upload/download)不能为空");
            return null;
        }
        if (StringUtils.isBlank(param.getLocalPath()) || StringUtils.isBlank(param.getRemotePath())) {
            XxlJobHelper.log("参数校验失败：本地目录和远程目录不能为空");
            return null;
        }
        if (StringUtils.isBlank(param.getFileNamePattern())) {
            XxlJobHelper.log("参数校验失败：文件名筛选模式不能为空");
            return null;
        }
        if (StringUtils.isBlank(param.getLocalTimezone()) || StringUtils.isBlank(param.getRemoteTimezone())) {
            XxlJobHelper.log("参数校验失败：本地时区和远程时区不能为空");
            return null;
        }
        if (!param.getLocalTimezone().matches("^(UTC|GMT)[+-]\\d+$")) {
            XxlJobHelper.log("参数校验失败：时区格式无效（应为UTC±N或GMT±N），当前: {}", param.getLocalTimezone());
            return null;
        }
        if (StringUtils.isBlank(param.getFileCompareMode())) {
            XxlJobHelper.log("参数校验失败：文件比较模式不能为空");
            return null;
        }
        if (StringUtils.isBlank(param.getTransferMode())) {
            XxlJobHelper.log("参数校验失败：传输模式不能为空");
            return null;
        }

        return param;
    }
}